iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0

今天要來介紹如何在 Python 寫單元測試(Unit Test),會使用到 pytest 這個第三方套件,其實 Python 自己也有一個套件 unitttest,但我自己是覺得 pytest 剛開始寫起來比較簡單易懂點,所以選擇他。有興趣看他們的詳細比較的話,可以參考這篇文章:

單元測試(Unit Test)

在介紹怎麼使用 pytest 之前,也來介紹一下為什麼我們會需要單元測試。第一個我覺得最重要的是,能確保別人在改完我們的程式碼後,一樣能通過我們的測試,來證明程式碼的功能還是正確的,讓我們對我們寫的程式碼更有信心不會隨便壞掉。
第二個是他能幫助我們一次測試所有可能的條件,舉例來說,我們在測試一個 function get_season_param_str 的時候,可能會因為傳入的參數的不同,有不同的結果,像是 get_season_param_str("2024")get_season_param_str(["2023", "2024"]),有了單元測試我們就能一次解決,不用為了特別一個 Case 一直改 example.py 去測試。
另外單元測試也能連例外錯誤(Exceptions)一起測試,確定是否有正確的錯誤訊息提示產生。

pytest

安裝
安裝的話跟前面的套件安裝一樣,使用 pip 安裝後就能使用,安裝完可以使用 pytest --version 來確認版本看是否也安裝成功。

# 安裝
pip install pytest
# 確認版本
pytest --version

檔案命名
安裝完 pytest 後,我們會需要建立一個 tests 資料夾當進入點,之後要跑測試指令就會需要指行 pytest tests/。再來就是我們測試的結構會需要跟我們 src 裡的一樣,不過檔名前面會需要加 test 的前綴。像是 utils 資料夾裡的 statcast.py 就要改成 test_statcast.py

Assert
接下來先來寫 utils/statcast.py 的測試 test_statcast.py。需要測試我們裡面的 function,寫法會是 importutils/statcast.py 裡的 function,然後建立測試的 function test_get_season_param_str,這個 function 會希望盡量命名成有意義的句子,這樣一個 function 就代表一個 Test Case,像是這個就是說 test get_season_param_str
再來會用 assert 來驗證是否回傳我們預期的數值,一個簡單的測試就算完成。

def test_get_season_param_str():
    # == 後面是我們手動輸入的預期結果
    assert get_season_param_str("2024") == "2024"

接下來執行 pytest tests/ 後,pytest 就會自動偵測到 test 檔去跑測試,但第一次可能會看到 import 錯誤:
https://ithelp.ithome.com.tw/upload/images/20241007/20163024pNQRFFjJtq.png

為了解決這個問題,會要去修改 pytest 的搜尋路徑,會需要去設定檔 pyproject.toml 去修改,讓 pytestsrc 開始去找 import

# pyproject.toml
+ [tool.pytest.ini_options]
+ pythonpath = [".", "src"]

增加這一段後,就能在測試檔裡面寫 from baseball_stats_python.utils.statcast import get_season_param_str,改完後再一次執行 pytest 就能得到:
https://ithelp.ithome.com.tw/upload/images/20241007/20163024NwmNAmLnAo.png

可以看到會顯示 1 passed 的訊息,代表我們有一個 Test Case 然後通過了。

測試例外

除了測試正常的回傳結果,我們也能寫如果我們傳入錯誤的參數的話,能否會 raise 跟我們預期的錯誤,要達到這樣的效果,會需要使用到 pytestraises,寫法會像是:

def test_get_season_param_str_invalid():
    with pytest.raises(ValueError) as e:
        get_season_param_str("2024.5")
    assert str(e.value) == "Invalid season: 2024.5"

    with pytest.raises(ValueError) as e:
        get_season_param_str(["2024.5", "2023"])
    assert str(e.value) == "Invalid seasons: 2024.5|2023"

會再另外宣告另一個 Test Case test_get_season_param_str_invalid,再來用 pytest.raises 來獲得錯誤訊息 e,最後回到使用 assert 來看錯誤訊息是否跟我們預期的一樣。

紅綠燈開發(Red-Green-Refactor)

像剛剛那樣都是我們已經在寫完 function 的時候再補測試,但其實還有另外一種開發的方式,就是先把測試寫好,再根據測試慢慢補上功能。因為測試成功會顯示綠色的 Passed,失敗的話會顯示紅色的 Failed,所以這樣的開發方式叫做紅綠燈開發。是 測試驅動開發(Test Driven Development) 的一種,大家也可以試試看。

本日小結

在開發開源文件的時候,測試是一個非常重要的一個環節,因為有可能會有各種不同的開發者進行程式碼開發,寫單元測試是最能確保所有功能在開發的同時還能正常運作的最佳方法。另外我們也能配合之前介紹的 Git Actions 來達成測試的自動化,讓我們每次有新的更新都跑一次測試檢查。測試的種類還有很多,以後有機會會再有更詳細的介紹。

最後一樣感謝大家耐心地看完今天的文章,有任何問題與建議非常歡迎留言告訴我,明天見,掰掰。


上一篇
Day 22 - Enum 延伸與套件 Import
下一篇
Day 24 - 為套件撰寫文件(Markdown & Github Wiki)
系列文
上次介紹的棒球套件很少更新了,那就只好自己寫一個!?31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言